#
#    Ceibal Chess - A chess activity for Sugar.
#    Copyright (C) 2008, 2009 Alejandro Segovia <asegovi@gmail.com>
#
#   This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
#

import pygame, os
from board import *
from errors import UndoError
from resourcemanager import image_manager

# Move classes
class Move:
	'''Represents a regular piece movement in chess.'''
	def __init__(self, (from_c, from_r), (to_c, to_r)):
		self.type = "Move"
		self.from_c = from_c
		self.from_r = from_r
		self.to_c = to_c
		self.to_r = to_r
		self.peformed = False
	
	def __eq__(self, other):
		'''Compare instances. Two moves are equal if they take a piece from the
		cell to the same cell'''
		return self.from_r == other.from_r and self.from_c == other.from_c and \
				self.to_r == other.to_r and self.to_c == other.to_c
	
	def __ne__(self, other):
		'''Compare instances. Two moves are not equal if they are not equal.'''
		return not (self == other)
	
	def perform(self, board):
		'''Perform a move on a board.'''
		self.src_piece = board[self.from_c][self.from_r].piece
		self.dst_piece = board[self.to_c][self.to_r].piece
		
		board[self.to_c][self.to_r].piece = self.src_piece
		board[self.from_c][self.from_r].piece = None
		
		self.performed = True
	
	def undo(self, board):
		'''Undo the effects of a move on a board.'''
		if not self.performed:
			raise UndoError("Move never performed: from (%d,%d) to (%d,%d)" % \
								(self.from_c, self.from_r, self.to_c, self.to_r))
								
		board[self.from_c][self.from_r].piece = self.src_piece
		board[self.to_c][self.to_r].piece = self.dst_piece
		
		self.performed = False

class Castling:
	'''Represents a Castling move in chess.'''
	def __init__(self, castling_type):
		'''Create a new instance of a Castling Move. Valid castling_types
			are "left" and "right"'''
		if not castling_type in ["left", "right"]:
			raise Exception("Invalid Castling type: " + castling_type)
		self.castling_type = castling_type
		self.type = "Castling"
		self.performed = False
	
	def __eq__(self, other):
		'''Compare instances. Two castling instances are equal if they are the
		same type'''
		return self.castling_type == other.castling_type
	
	def __ne__(self, other):
		'''Compare instances. Two castling instances are not equal if they are
		not the same type'''
		return self.castling_type != other.castling_type
	
	def perform(self, board):
		'''Perform a Castling move on a board.'''
		j = board.current_turn == "white" and 7 or 0
		
		if self.castling_type == "left":
			board[2][j].piece = board[4][j].piece
			board[4][j].piece = None
			board[3][j].piece = board[0][j].piece
			board[0][j].piece = None
		elif self.castling_type == "right":
			board[6][j].piece = board[4][j].piece
			board[4][j].piece = None
			board[5][j].piece = board[7][j].piece
			board[7][j].piece = None
		else:
			raise MoveError("Invalid Castling type. Valid types are 'left' and 'right'")
		
		self.performed = True
	
	def undo(self, board):
		'''Undo a crowning move.'''
		j = board.current_turn == "white" and 7 or 0
		
		if self.castling_type == "left":
			board[4][j].piece = board[2][j].piece
			board[2][j].piece = None
			board[0][j].piece = board[3][j].piece
			board[3][j].piece = None
		elif self.castling_type == "right":
			board[4][j].piece = board[6][j].piece
			board[6][j].piece = None
			board[7][j].piece = board[5][j].piece
			board[5][j].piece = None
		else:
			raise MoveError("Invalid Castling type. Valid types are 'left' and 'right'")
		
		self.performed = False

class Crowning:
	'''Represents a Pawn Crowning move in chess.'''
	def __init__(self, (from_c, from_r), (to_c, to_r), piece):
		self.type = "Crowning"
		self.from_c = from_c
		self.from_r = from_r
		self.to_c = to_c
		self.to_r = to_r
		self.piece = piece
		self.performed = False
	
	def __eq__(self, other):
		'''Compare instances. Two Crowning moves are equal if they move from
		the same cell to the same cell and crown into the same piece'''
		return self.from_c == other.from_c and self.from_r == other.from_r and \
			self.to_c == other.to_c and self.to_r == other.to_r and \
			self.piece == other.piece
	
	def __ne__(self, other):
		'''Compare instances. Two Crowning moves are not equal if they are not
		equal.'''
		return not (self == other)
	
	def perform(self, board):
		'''Perform this move on a board.'''
		self.src_piece = board[self.from_c][self.from_r].piece
		self.dst_piece = board[self.to_c][self.to_r].piece
		
		board[self.to_c][self.to_r].piece = self.piece
		board[self.from_c][self.from_r].piece = None
		self.performed = True
	
	def undo(self, board):
		'''Undo the effects of this move on a board.'''
		if not self.performed:
			raise UndoError("Castling never performed: from (%d,%d) to (%d,%d)" % \
								(self.from_c, self.from_r, self.to_c, self.to_r))
								
		board[self.from_c][self.from_r].piece = self.src_piece
		board[self.to_c][self.to_r].piece = self.dst_piece
		
		self.performed = False

WHITE_RANK = 7
BLACK_RANK = 0

class Piece:
	
	def __init__(self, type, owner):
		self.type = type
		self.picked = False
		self.owner = owner
		if (type == "pawn"):
			self.en_passant = False
		self.has_moved = False
		
	def render(self, surface, x, y, cell_size):
		'''
		Render this piece. x,y are the top left corner of the 
		container cell; cell_size is its size
		'''
		img = image_manager.get_image(self.type + self.owner + ".png")

		w,h = img.get_width(), img.get_height()
		tx = x + (cell_size - w) / 2
		ty = y + (cell_size - h) / 2
		
		surface.blit(img, pygame.Rect(tx, ty, w, h))
		
	def on_pick(self):
		return

		
	def get_moves(self, column, row, board, attack_only=False):
		'''Get all possible cell destinations depending on i, j and 
		the piece type. Special parameter attack_only is used to request 
		"attack_only moves" only, excluding pawn forward movement and 
		castling.'''
		
		#if not board.hypothetical:
		#	print "Calculating moves for", self.type
		
		dests = []
		
		dests.append((column, row))

		if self.type == "knight":
			dests.append((column+1, row+2))
			dests.append((column+1, row-2))
			dests.append((column-1, row-2))
			dests.append((column-1, row+2))
			dests.append((column+2, row+1))
			dests.append((column+2, row-1))
			dests.append((column-2, row-1))
			dests.append((column-2, row+1))
			
		if self.type == "rook" or self.type == "queen":
			for dc,dr in ([(0,d) for d in [-1,1]] + [(d,0) for d in [-1,1]]):
				c = column + dc
				r = row + dr
				blocked = None
				while r >= 0 and r < 8 and c >= 0 and c < 8 and not blocked:
					blocked = board.board[c][r].piece
					if not blocked or self.owner != board.board[c][r].piece.owner \
						or (self.type == "rook" and board.board[c][r].piece.type == "king"):
						dests.append((c,r))
						c = c + dc
						r = r + dr
						
				
		if self.type == "bishop" or self.type == "queen":
			for dc,dr in [(dc,dr) for dc in [-1,1] for dr in [-1,1]]:
				c = column + dc
				r = row + dr
				blocked = None
				while r >= 0 and r < 8 and c >= 0 and c < 8 and not blocked:
					blocked = board.board[c][r].piece
					if not blocked or self.owner != board.board[c][r].piece.owner :
						dests.append((c,r))
						c = c + dc
						r = r + dr
				
		if self.type == "king":
			dests = [(column + dc, row + dr) for dc in [-1,0,1] for dr in [-1,0,1] if dc != 0 or dr != 0]
			
			if not attack_only:
				threats = board.get_all_attacked_by_moves(self.owner)
				
				#Castling
				if not board.king_is_checked(self.owner):
				
					if self.owner == "white":
						if not board.white_king_moved:
							if not board.white_tower_right_moved and \
							not board.board[column+1][row].piece and \
							not board.board[column+2][row].piece and \
							(column+2, row) not in threats and\
							(column+1, row) not in threats:
								dests.append((column+2, row))
							
							if not board.white_tower_left_moved and \
							not board.board[column-1][row].piece and \
							not board.board[column-2][row].piece and \
							not board.board[column-3][row].piece and \
							(column-2, row) not in threats and \
							(column-1, row) not in threats:
								dests.append((column-2, row))
					else:
						if not board.black_king_moved:
							if not board.black_tower_right_moved and \
							not board.board[column+1][row].piece and \
							not board.board[column+2][row].piece and \
							(column+2,row) not in threats and\
							(column+1,row) not in threats:
								dests.append((column+2, row))
							if not board.black_tower_left_moved and \
							not board.board[column-1][row].piece and \
							not board.board[column-2][row].piece and \
							not board.board[column-3][row].piece and \
							(column-2, row) not in threats and\
							(column-1, row) not in threats:
								dests.append((column-2, row))

				#Remove threatened destinations:
				#all_dests = len(dests)
				dests = filter(lambda x:(x[0],x[1]) not in threats, dests)			
				#print "Removed", all_dests - len(dests), "inmediate threats for king"
		
		if self.type == "pawn":
			dr = self.owner == "white" and -1 or 1
			#rank = self.owner != "white" and BLACK_RANK or WHITE_RANK
			if self.owner == "white":
				rank = WHITE_RANK
			else:
				rank = BLACK_RANK

			#Bound checking:
			if 0 <= row + dr < 8:
				if not board[column][row + dr].piece and not attack_only:
					dests.append((column, row + dr))
				
				#Double step:
				if row== rank + dr and not board[column][row + 2*dr].piece\
					and not board[column][row + 1*dr].piece and not attack_only:
					dests.append((column, row + 2 * dr))
				
				#Attack moves:
				if attack_only:
					dests.append((column - 1, row + dr))
					dests.append((column + 1, row + dr))
				else:
					if column > 0 and (board.board[column - 1][row + dr].piece):
						if board.board[column - 1][row + dr].piece.owner != self.owner:
							dests.append((column - 1, row + dr))
					if column < 7 and (board.board[column + 1][row + dr].piece):
						if board.board[column + 1][row + dr].piece.owner != self.owner:
							dests.append((column + 1, row + dr))
							
				#Attack en passant:
				if self.owner == "white" and board.black_en_passant and row == 3:
					dests.append((board.black_en_passant, 2))
					
				elif self.owner == "black" and board.white_en_passant and row == 4:
					dests.append((board.white_en_passant, 5))
		
		
		#Remove out-of-bounds destinations
		dests = filter(lambda x: x[0] >= 0 and x[0] < 8 and x[1] >= 0 and x[1] < 8, dests)

		#Remove moving to the cell we are in
		dests = filter(lambda x: x != (column, row), dests)

		#Remove occupied destinations:
		dests = filter(lambda x:board[x[0]][x[1]].piece is None or \
				board[x[0]][x[1]].piece.owner != self.owner, dests)

		#Simulate movement and remove destinations that leave the king checked:
		if not board.hypothetical:
			invalid = []
			for i,j in dests:
				next = board.clone()
				is_valid = next.move_piece_in_cell_to(next[column][row], i, j)
				if not is_valid or next.king_is_checked(self.owner):
					invalid.append((i,j))
			dests = filter(lambda x:x not in invalid, dests)


		return dests
	
	def is_turn(self, lastowner):
		if self.owner == lastowner:
			return True
		else:
			return False

	def filter_invalid(self, move_list, column, row, board):
		'''Given a board, filter invalid moves from the a move list.'''
		#Remove out-of-bounds destinations
		move_list = filter(lambda x: x[0] >= 0 and x[0] < 8 and x[1] >= 0 and x[1] < 8, move_list)

		#Remove moving to the cell we are in
		move_list = filter(lambda x: x != (column, row), dests)

		#Remove occupied destinations:
		move_list = filter(lambda x:board[x[0]][x[1]].piece is None or \
				board[x[0]][x[1]].piece.owner != self.owner, dests)
		
		return move_list

class BasePiece:
	'''Base class for pieces.'''
	def __init__(self):
		'''Create a new instance of Piece.'''
		self.move_cache = []
		self.attack_cache = []
	
	def filter_invalid(self, moves, board):
		'''Filter out moves that fall outside the board as well as moves into 
			cells occupied by pieces from the same owner.'''
		
		valid_moves = []
		for move in moves:
			if move.type in ["Move", "Crowning"]:
				if not (0 <= move.to_r <= 7):
					continue
				
				if not (0 <= move.to_c <= 7):
					continue
				
				if (move.to_r == move.from_r) and (move.to_c == move.from_c):
					continue
				
				to_c, to_r = move.to_c, move.to_r
				c, r = move.from_c, move.from_r
				
				if not board[c][r].piece:
					raise Exception("Invalid movement: there is no piece at (%d,%d)" % (c,r))
				
				if board[to_c][to_r].piece and \
					board[to_c][to_r].piece.owner == board[c][r].piece.owner:
					continue
				valid_moves.append(move)
				
			elif move.type == "Castling":
				valid_moves.append(move)
		
		return valid_moves
				


class Knight(BasePiece):
	'''Representation of the Knight piece.'''
	
	def __init__(self, owner):
		'''Create a new instance of Knight. owner may be "white" or "black".'''
		self.owner = owner

	def clone(self):
		'''Clone this piece into a new independent instance.'''
		return Knight(self.owner)

	def get_moves(self, col, row, board, attack_only = False):
		
		dests = []
		
		dests.append(Move((col, row), (col+1, row+2)))
		dests.append(Move((col, row), (col+1, row-2)))
		dests.append(Move((col, row), (col-1, row-2)))
		dests.append(Move((col, row), (col-1, row+2)))
		dests.append(Move((col, row), (col+2, row+1)))
		dests.append(Move((col, row), (col+2, row-1)))
		dests.append(Move((col, row), (col-2, row-1)))
		dests.append(Move((col, row), (col-2, row+1)))
		
		return self.filter_invalid(dests, board)

class Rook(BasePiece):
	'''Representation of the Rook piece.'''

	def __init__(self, owner):
		'''Create a new instance of Rook. owner may be "white" or "black".'''
		self.owner = owner

	def clone(self):
		'''Clone this piece into a new independent instance.'''
		return Rook(self.owner)

	def get_moves(self, col, row, board, attack_only = False):
		dests = []
		for dc,dr in ([(0,d) for d in [-1,1]] + [(d,0) for d in [-1,1]]):
				c = col + dc
				r = row + dr
				blocked = None
				while r >= 0 and r < 8 and c >= 0 and c < 8 and not blocked:
					blocked = board[c][r].piece
					if not blocked or self.owner != board[c][r].piece.owner \
						or (self.type == "rook" and board[c][r].piece.type == "king"):
						dests.append(Move((col, row), (c,r)))
						c = c + dc
						r = r + dr
		
		return self.filter_invalid(dests, board)

class Bishop(BasePiece):
	'''Representation of the Bishop piece.'''

	def __init__(self, owner):
		'''Create a new instance of Bishop. owner may be "white" or "black".'''
		self.owner = owner
		
	def clone(self):
		'''Clone this piece into a new independent instance.'''
		return Bishop(self.owner)

	def get_moves(self, col, row, board, attack_only = False):
		dests = []
		for dc,dr in [(dc,dr) for dc in [-1,1] for dr in [-1,1]]:
			c = col + dc
			r = row + dr
			blocked = None
			while r >= 0 and r < 8 and c >= 0 and c < 8 and not blocked:
				blocked = board[c][r].piece
				if not blocked or self.owner != board[c][r].piece.owner :
					dests.append(Move((col, row), (c,r)))
					c = c + dc
					r = r + dr
		
		return self.filter_invalid(dests, board)

class Queen(BasePiece):
	'''Representation of the Queen piece'''

	def __init__(self, owner):
		'''Create a new instance of Queen. owner may be "white" or "black".'''
		self.owner = owner

	def clone(self):
		'''Clone this piece into a new independent instance.'''
		return Queen(self.owner)

	def get_moves(self, col, row, board, attack_only = False):
		dests = []
		for dc,dr in ([(0,d) for d in [-1,1]] + [(d,0) for d in [-1,1]]):
			c = col + dc
			r = row + dr
			blocked = None
			while r >= 0 and r < 8 and c >= 0 and c < 8 and not blocked:
				blocked = board[c][r].piece
				if not blocked or self.owner != board[c][r].piece.owner:
					dests.append(Move((col, row), (c,r)))
					c = c + dc
					r = r + dr
		
		for dc,dr in [(dc,dr) for dc in [-1,1] for dr in [-1,1]]:
			c = col + dc
			r = row + dr
			blocked = None
			while r >= 0 and r < 8 and c >= 0 and c < 8 and not blocked:
				blocked = board[c][r].piece
				if not blocked or self.owner != board[c][r].piece.owner :
					dests.append(Move((col, row), (c,r)))
					c = c + dc
					r = r + dr
		
		return self.filter_invalid(dests, board)

class King(BasePiece):
	'''Representation of the King piece'''

	def __init__(self, owner):
		'''Create a new instance of King. owner may be "white" or "black".'''
		self.owner = owner

	def clone(self):
		'''Clone this piece into a new independent instance.'''
		return King(self.owner)

	def get_moves(self, col, row, board, attack_only = False):
		dests = [Move((col, row), (col + dc, row + dr)) \
			for dc in [-1,0,1] for dr in [-1,0,1] if dc != 0 or dr != 0]
			
		if not attack_only:
			threats = board.get_all_attacked_by_moves(self.owner)
			
			#Castling
			if not board.king_is_checked(self.owner):
			
				if self.owner == "white":
					if not board.white_king_moved:
						if not board.white_tower_right_moved and \
						not board[col+1][row].piece and \
						not board[col+2][row].piece and \
						(col+2, row) not in threats and\
						(col+1, row) not in threats:
							dests.append(Castling("right"))
						
						if not board.white_tower_left_moved and \
						not board[col-1][row].piece and \
						not board[col-2][row].piece and \
						not board[col-3][row].piece and \
						(col-2, row) not in threats and \
						(col-1, row) not in threats:
							dests.append(Castling("left"))
				else:
					if not board.black_king_moved:
						if not board.black_tower_right_moved and \
						not board[col+1][row].piece and \
						not board[col+2][row].piece and \
						(col+2,row) not in threats and\
						(col+1,row) not in threats:
							dests.append(Castling("right"))
						
						if not board.black_tower_left_moved and \
						not board[col-1][row].piece and \
						not board[col-2][row].piece and \
						not board[col-3][row].piece and \
						(col-2, row) not in threats and\
						(col-1, row) not in threats:
							dests.append(Castling("left"))
			
			dests = filter(lambda x:(x.to_c,x.to_r) not in threats, dests)
		return self.filter_invalid(dests, board)

class Pawn(BasePiece):
	'''Representation of the Pawn piece.'''

	def __init__(self, owner):
		'''Create a new instance of Pawn. owner may be "white" or "black".'''
		self.owner = owner
		self.en_passant = False

	def clone(self):
		'''Clone this piece into a new independent instance.'''
		return Pawn(self.owner)

	def get_moves(self, col, row, board, attack_only = False):
		dests = []
		dr = self.owner == "white" and -1 or 1
		rank = self.owner != "white" and BLACK_RANK or WHITE_RANK		

		#Bound checking:
		if 0 <= row + dr < 8:
			#Normal move or Crowning?
			if row + dr != 0 and row + dr != 7:
				if not board[col][row + dr].piece and not attack_only:
					dests.append(Move((col, row), (col, row + dr)))
			else:
				if not board[col][row + dr].piece and not attack_only:
					dests.append(Crowning((col, row), (col, row + dr), Queen(self.owner)))
			
			#Double step:
			if row == rank + dr and not board[col][row + 2*dr].piece \
				and not board[col][row + 1*dr].piece and not attack_only:
				dests.append(Move((col, row), (col, row + 2 * dr)))
			
			#Attack moves:
			if col > 0:
				p = board[col-1][row+dr].piece
				if p and p.owner != self.owner:
					dests.append(Move((col, row), (col - 1, row + dr)))
					
			if col < 7:
				p = board.board[col+1][row+dr].piece
				if p and p.owner != self.owner:
					dests.append(Move((col, row), (col + 1, row + dr)))
						
			#Attack en passant:
			if self.owner == "white" and board.black_en_passant and row == 3:
				dests.append(Move((col, row), (board.black_en_passant, 2)))
				
			elif self.owner == "black" and board.white_en_passant and row == 4:
				dests.append(Move((col, row), (board.white_en_passant, 5)))
		
		return self.filter_invalid(dests, board)

